

#include "stb_image_source.h"

#define STB_IMAGE_IMPLEMENTATION
#include "stb/stb_image.h"

#define STB_IMAGE_RESIZE_IMPLEMENTATION
// Least amount of fuzziness on upscaling
#define STBIR_DEFAULT_FILTER_UPSAMPLE STBIR_FILTER_BOX

// There is an old and a new version of stb resize. Prefer newer if available
// (tested for in CMake), and provide adapter functions to use either.
#if STB_RESIZE_VERSION2
#include "stb/stb_image_resize2.h"
inline void stb_resize_image(const unsigned char *input_pixels, int input_w,
                             int input_h, unsigned char *output_pixels,
                             int output_w, int output_h) {
  static constexpr stbir_pixel_layout kFramebufferFormat = STBIR_RGBA;
  stbir_resize_uint8_linear(input_pixels, input_w, input_h, 0, //
                            output_pixels, output_w, output_h, 0,
                            kFramebufferFormat);
}

#else
#include "stb/stb_image_resize.h"
inline void stb_resize_image(const unsigned char *input_pixels, int input_w,
                             int input_h, unsigned char *output_pixels,
                             int output_w, int output_h) {
  static constexpr int kFramebufferFormat = 4;             // RGBA.
  stbir_resize_uint8(input_pixels, input_w, input_h, 0,    //
                     output_pixels, output_w, output_h, 0, //
                     kFramebufferFormat);
}
#endif

// TODO: preprocessed frame and SendFrames() are similar to
// image-display.cc. Maybe things can be consolidated.

static constexpr int kDesiredChannels = 4; // RGBA, our framebuffer format
namespace tv {
class STBImageSource::PreprocessedFrame {
public:
  PreprocessedFrame(const uint8_t *image_data, int source_w, int source_h,
                    int target_w, int target_h, const Duration &delay,
                    const DisplayOptions &opt)
      : delay_(delay), framebuffer_(target_w, target_h) {
    stb_resize_image(image_data, source_w, source_h,
                     (uint8_t *)framebuffer_.begin(), target_w, target_h);
    framebuffer_.AlphaComposeBackground(
        opt.bgcolor_getter, opt.bg_pattern_color,
        opt.pattern_size * opt.cell_x_px, opt.pattern_size * opt.cell_y_px / 2);
  }
  Duration delay() const { return delay_; }
  const tv::Framebuffer &framebuffer() const { return framebuffer_; }

private:
  const Duration delay_;
  tv::Framebuffer framebuffer_;
};

STBImageSource::STBImageSource(const std::string &filename)
    : ImageSource(filename) {}

STBImageSource::~STBImageSource() {
  for (PreprocessedFrame *f : frames_)
    delete f;
}

std::string
STBImageSource::FormatTitle(const std::string &format_string) const {
  return FormatFromParameters(format_string, filename_, orig_width_,
                              orig_height_, "stb");
}

// NB: Animation is using some internal API of stb image, so is somewhat
// dependent on the implemantation used; but since we ship it in third_party/,
// this is not an issue.
bool STBImageSource::LoadAndScale(const DisplayOptions &options,
                                  int frame_offset, int frame_count) {
#ifdef WITH_TV_VIDEO
  if (LooksLikeAPNG(filename())) {
    return false; // STB can't apng animate. Let Video do it.
  }
#endif

  FILE *img_file = stbi__fopen(filename().c_str(), "rb");
  if (!img_file)
    return false;

  stbi__context context;
  stbi__start_file(&context, img_file);

  int channels;
  int target_width = 0;
  int target_height = 0;

  // There is no public API yet for accessing gif, so use some internals.
  if (stbi__gif_test(&context)) {
    stbi__gif gdata;
    memset(&gdata, 0, sizeof(gdata));
    uint8_t *data;
    while ((data = stbi__gif_load_next(&context, &gdata, &channels,
                                       kDesiredChannels, 0))) {
      if (data == (const uint8_t *)&context)
        break;
      orig_width_ = gdata.w;
      orig_height_ = gdata.h;

      CalcScaleToFitDisplay(gdata.w, gdata.h, options, false, &target_width,
                            &target_height);
      frames_.push_back(new PreprocessedFrame(
          data, gdata.w, gdata.h, target_width, target_height,
          Duration::Millis(gdata.delay), options));
    }
    STBI_FREE(gdata.out);
    STBI_FREE(gdata.history);
    STBI_FREE(gdata.background);
  } else {
    int w, h;
    uint8_t *data = stbi__load_and_postprocess_8bit(&context, &w, &h, &channels,
                                                    kDesiredChannels);
    if (!data) {
      fclose(img_file);
      return false;
    }

    orig_width_ = w;
    orig_height_ = h;

    CalcScaleToFitDisplay(w, h, options, false, &target_width, &target_height);
    frames_.push_back(new PreprocessedFrame(
        data, w, h, target_width, target_height, Duration(), options));
    stbi_image_free(data);
  }

  indentation_ =
      options.center_horizontally ? (options.width - target_width) / 2 : 0;

  max_frames_ = (frame_count < 0) ? (int)frames_.size()
                                  : std::min(frame_count, (int)frames_.size());

  fclose(img_file);

  return !frames_.empty();
}

void STBImageSource::SendFrames(const Duration &duration, int loops,
                                const volatile sig_atomic_t &interrupt_received,
                                const Renderer::WriteFramebufferFun &sink) {
  int last_height = -1; // First image emit will not have a height.
  const bool is_animation = frames_.size() > 1;
  if (frames_.size() == 1 || !is_animation)
    loops = 1; // If there is no animation, nothing to repeat.

  // Not initialized or negative value wants us to loop forever.
  // (note, kNotInitialized is actually negative, but here for clarity
  const bool loop_forever = (loops < 0) || (loops == tv::kNotInitialized);

  tv::Duration time_from_first_frame;
  bool is_first = true;
  for (int k = 0; (loop_forever || k < loops) && !interrupt_received &&
                  time_from_first_frame < duration;
       ++k) {
    for (int f = 0; f < max_frames_ && !interrupt_received; ++f) {
      const auto &frame = frames_[f];
      time_from_first_frame.Add(frame->delay());
      const int dx = indentation_;
      const int dy = is_animation && last_height > 0 ? -last_height : 0;
      SeqType seq_type = SeqType::FrameImmediate;
      if (is_animation) {
        seq_type =
            is_first ? SeqType::StartOfAnimation : SeqType::AnimationFrame;
      }
      sink(dx, dy, frame->framebuffer(), seq_type,
           std::min(time_from_first_frame, duration));
      last_height = frame->framebuffer().height();
      if (time_from_first_frame > duration)
        break;
      is_first = false;
    }
  }
}
} // namespace tv
